// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
//
//   Keepass2Android is free software: you can redistribute it and/or modify
//   it under the terms of the GNU General Public License as published by
//   the Free Software Foundation, either version 3 of the License, or
//   (at your option) any later version.
//
//   Keepass2Android is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU General Public License for more details.
//
//   You should have received a copy of the GNU General Public License
//   along with Keepass2Android.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using KeePassLib.Serialization;
using KeePassLib.Utility;
#if !EXCLUDE_JAVAFILESTORAGE
using Keepass2android.Javafilestorage;
#endif
using Exception = System.Exception;
using FileNotFoundException = Java.IO.FileNotFoundException;
using KeePass.Util;

namespace keepass2android.Io
{
#if !EXCLUDE_JAVAFILESTORAGE
  public abstract class JavaFileStorage : IFileStorage, IPermissionRequestingFileStorage
  {
    protected string Protocol { get { return _jfs.ProtocolId; } }

    public virtual IEnumerable<string> SupportedProtocols { get { yield return Protocol; } }
    public abstract bool UserShouldBackup { get; }


    protected readonly IJavaFileStorage _jfs;
    private readonly IKp2aApp _app;

    public JavaFileStorage(IJavaFileStorage jfs, IKp2aApp app)
    {
      _jfs = jfs;
      _app = app;
    }

    public void Delete(IOConnectionInfo ioc)
    {
      try
      {
        Jfs.Delete(IocToPath(ioc));
      }
      catch (FileNotFoundException e)
      {
        throw new System.IO.FileNotFoundException(ExceptionUtil.GetErrorMessage(e), e);
      }
      catch (Java.Lang.Exception e)
      {
        throw LogAndConvertJavaException(e);
      }
    }

    public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion)
    {
      return false;

      //commented because this currently might use the network which is not permitted here
      /*try
      {
          return Jfs.CheckForFileChangeFast(ioc.Path, previousFileVersion);
      }
      catch (Java.Lang.Exception e)
      {
          throw LogAndConvertJavaException(e);
      }*/

    }

    public string GetCurrentFileVersionFast(IOConnectionInfo ioc)
    {
      try
      {
        return Jfs.GetCurrentFileVersionFast(IocToPath(ioc));
      }
      catch (Java.Lang.Exception e)
      {
        throw LogAndConvertJavaException(e);
      }
    }

    public Stream OpenFileForRead(IOConnectionInfo ioc)
    {
      try
      {
        return Jfs.OpenFileForRead(IocToPath(ioc));
      }
      catch (FileNotFoundException e)
      {
        throw new System.IO.FileNotFoundException(e.Message, e);
      }
      catch (Java.Lang.Exception e)
      {
        throw LogAndConvertJavaException(e);
      }
    }


    private Exception LogAndConvertJavaException(Exception e)
    {
      Kp2aLog.Log(e.Message);

      if (e is UserInteractionRequiredException)
        return e;
      //seems like UserInteractionRequiredException is not propagated correctly into the C# world, we can't catch it
      // -> rethrow correctly here
      // Note: the Contains-check looks a bit broad, but it should be safe
      if (e.ToString().Contains("keepass2android.javafilestorage.UserInteractionRequiredException"))
      {
        throw new UserInteractionRequiredException();
      }

      Java.Lang.Exception exception = e as Java.Lang.Exception;

      if ((exception is Java.Lang.InterruptedException) || (exception is Java.IO.InterruptedIOException))
      {
        throw new Java.Lang.InterruptedException(exception.Message);
      }
      if (exception != null)
      {
        var ex = new Exception(exception.LocalizedMessage ??
            e.Message ??
            _app.GetResourceString(UiStringKey.ErrorOcurred) + exception.GetType().Name, e);
        return ex;
      }

      return e;

    }

    public virtual IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction)
    {
      return new JavaFileStorageWriteTransaction(IocToPath(ioc), useFileTransaction, this);
    }

    internal IJavaFileStorage Jfs
    {
      get { return _jfs; }
    }


    class JavaFileStorageWriteTransaction : IWriteTransaction
    {
      private readonly string _path;
      private readonly bool _useFileTransaction;
      private readonly JavaFileStorage _javaFileStorage;
      private MemoryStream _memoryStream;

      public JavaFileStorageWriteTransaction(string path, bool useFileTransaction, JavaFileStorage javaFileStorage)
      {
        _path = path;
        _useFileTransaction = useFileTransaction;
        _javaFileStorage = javaFileStorage;
      }

      public void Dispose()
      {
        _memoryStream.Dispose();
      }

      public Stream OpenFile()
      {
        _memoryStream = new MemoryStream();
        return _memoryStream;
      }

      public void CommitWrite()
      {
        try
        {
          _javaFileStorage.Jfs.UploadFile(_path, _memoryStream.ToArray(), _useFileTransaction);
        }
        catch (Java.Lang.Exception e)
        {
          throw _javaFileStorage.LogAndConvertJavaException(e);
        }
      }
    }

    public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc)
    {
      return UrlUtil.StripExtension(
              _jfs.GetFilename(IocToPath(ioc)));
    }

    public string GetFileExtension(IOConnectionInfo ioc)
    {
      return UrlUtil.GetExtension(ioc.Path);
    }

    public bool RequiresCredentials(IOConnectionInfo ioc)
    {
      return false;
    }

    public void CreateDirectory(IOConnectionInfo ioc, string newDirName)
    {
      try
      {
        Jfs.CreateFolder(IocToPath(ioc), newDirName);
      }
      catch (FileNotFoundException e)
      {
        throw new System.IO.FileNotFoundException(ExceptionUtil.GetErrorMessage(e), e);
      }
      catch (Java.Lang.Exception e)
      {
        throw LogAndConvertJavaException(e);
      }
    }

    public IEnumerable<FileDescription> ListContents(IOConnectionInfo ioc)
    {
      try
      {
        IList<IJavaFileStorage.FileEntry> entries = Jfs.ListFiles(IocToPath(ioc));

        return entries.Select(ConvertToFileDescription);

      }
      catch (FileNotFoundException e)
      {
        throw new System.IO.FileNotFoundException(ExceptionUtil.GetErrorMessage(e), e);
      }
      catch (Java.Lang.Exception e)
      {
        throw LogAndConvertJavaException(e);
      }
    }

    private FileDescription ConvertToFileDescription(IJavaFileStorage.FileEntry e)
    {
      return new FileDescription
      {
        CanRead = e.CanRead,
        CanWrite = e.CanWrite,
        DisplayName = e.DisplayName,
        IsDirectory = e.IsDirectory,
        LastModified = JavaTimeToCSharp(e.LastModifiedTime),
        Path = e.Path,
        SizeInBytes = e.SizeInBytes
      };
    }

    public FileDescription GetFileDescription(IOConnectionInfo ioc)
    {
      try
      {
        return ConvertToFileDescription(Jfs.GetFileEntry(IocToPath(ioc)));
      }
      catch (FileNotFoundException e)
      {
        throw new System.IO.FileNotFoundException(ExceptionUtil.GetErrorMessage(e), e);
      }
      catch (Java.Lang.Exception e)
      {
        throw LogAndConvertJavaException(e);
      }
    }

    public bool RequiresSetup(IOConnectionInfo ioConnection)
    {
      return _jfs.RequiresSetup(IocToPath(ioConnection));
    }

    public virtual void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId)
    {
      Kp2aLog.Log("StartSelectFile " + protocolId);
      _jfs.StartSelectFile((IJavaFileStorage.IFileStorageSetupInitiatorActivity)activity, isForSave, requestCode);
    }

    public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode, Boolean alwaysReturnSuccess)
    {
      try
      {
        _jfs.PrepareFileUsage((IJavaFileStorage.IFileStorageSetupInitiatorActivity)activity, IocToPath(ioc), requestCode,
                              alwaysReturnSuccess);
      }
      catch (Exception e)
      {
        throw LogAndConvertJavaException(e);
      }
    }

    public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc)
    {
      try
      {
        _jfs.PrepareFileUsage(ctx, IocToPath(ioc));
      }
      catch (Exception e)
      {
        throw LogAndConvertJavaException(e);
      }

    }

    public bool IsPermanentLocation(IOConnectionInfo ioc)
    {
      return true;
    }

    public bool IsReadOnly(IOConnectionInfo ioc, OptionalOut<UiStringKey> reason = null)
    {
      return false; //TODO implement. note, however, that we MAY return false even if it's read-only
    }

    public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState)
    {
      _jfs.OnCreate(((IJavaFileStorage.IFileStorageSetupActivity)activity), savedInstanceState);
    }

    public void OnResume(IFileStorageSetupActivity activity)
    {
#if DEBUG
      Kp2aLog.Log("JFS/OnResume Ioc.Path=" + activity.Ioc.Path + ". Path=" + ((IJavaFileStorage.IFileStorageSetupActivity)activity).Path);
#endif
      _jfs.OnResume(((IJavaFileStorage.IFileStorageSetupActivity)activity));
    }

    public void OnStart(IFileStorageSetupActivity activity)
    {
      _jfs.OnStart(((IJavaFileStorage.IFileStorageSetupActivity)activity));
    }

    public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data)
    {
      _jfs.OnActivityResult(((IJavaFileStorage.IFileStorageSetupActivity)activity), requestCode, resultCode, data);
    }

    public string GetDisplayName(IOConnectionInfo ioc)
    {
      return _jfs.GetDisplayName(IocToPath(ioc));
    }

    public string CreateFilePath(string parent, string newFilename)
    {
      return _jfs.CreateFilePath(parent, newFilename);
    }

    public IOConnectionInfo GetParentPath(IOConnectionInfo ioc)
    {
      return IoUtil.GetParentPath(ioc);
    }

    public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename)
    {
      try
      {
        return IOConnectionInfo.FromPath(
        ListContents(folderPath).Where(desc => { return desc.DisplayName == filename; })
            .Single()
            .Path);
      }
      catch (Exception e)
      {
        throw new Exception("Error finding " + filename + " in " + GetDisplayName(folderPath), e);
      }

    }

    private DateTime JavaTimeToCSharp(long javatime)
    {
      return new DateTime(1970, 1, 1).AddMilliseconds(javatime);
    }

    public virtual string IocToPath(IOConnectionInfo ioc)
    {
      return ioc.Path;
    }

    public void OnRequestPermissionsResult(IFileStorageSetupActivity fileStorageSetupActivity, int requestCode,
        string[] permissions, Permission[] grantResults)
    {
      _jfs.OnRequestPermissionsResult(((IJavaFileStorage.IFileStorageSetupActivity)fileStorageSetupActivity), requestCode,
          permissions, grantResults.Select(p => (int)p).ToArray());
    }
  }
#endif
}